package com.basker.pisces.core.serialize.json;

import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.serializer.SerializeWriter;
import com.basker.pisces.core.DataContractGenerator;
import com.basker.pisces.core.IDataContract;
import com.basker.pisces.core.IInitialize;
import com.basker.pisces.core.enums.DataObjectState;
import com.basker.pisces.core.meta.ICollectionField;
import com.basker.pisces.core.meta.IDataField;
import com.basker.pisces.core.meta.IDataObjectField;
import com.basker.pisces.core.meta.IDataObjectMeta;
import com.basker.pisces.core.meta.IField;
import com.basker.pisces.core.meta.IReferenceField;
import com.basker.pisces.core.utils.ConvertUtils;
import com.basker.pisces.utils.EnumUtils;
import com.basker.pisces.utils.PageUtils;
import com.basker.pisces.utils.ReflectionUtils;

public class DataContractArraySerializer {

    private static final Logger logger = LoggerFactory.getLogger(DataContractArraySerializer.class);

    private static final String VERSION = "V1.2";

    private IDataObjectMeta objectMeta;

    public DataContractArraySerializer(IDataObjectMeta objectMeta) {
        Assert.notNull(objectMeta, "parameter 'objectMeta' is required");
        this.objectMeta = objectMeta;
    }

    public Object deserializeFromArray(List<Object> objectArray) {
        if (objectArray == null) {
            return null;
        }

        return deserialize0(this.objectMeta, objectArray);
    }

    public Object deserializeFromString(String source) {
        if (StringUtils.isEmpty(source)) {
            return null;
        }

        JSONArray objectArray = null;

        try {
            objectArray = JSON.parseArray(source);
        } catch (JSONException ex) {
            if (logger.isWarnEnabled()) {
                logger.warn("'" + source + "' can not be parse to jsonarray!");
            }

            return null;
        }

        return deserializeFromArray(objectArray);
    }

    public String serializeToString(Object dataObject) {
        // JSONArray.toJSONString出乎意料得慢，于是自己拼接json字符串

        if (dataObject == null) {
            return null;
        }

        try {
            if (dataObject instanceof IDataContract) {
                ((IDataContract) dataObject).beginInit();
            }

            return serialize1(this.objectMeta, dataObject);
        } finally {
            if (dataObject instanceof IDataContract) {
                ((IDataContract) dataObject).endInit();
            }
        }

    }

    private void addCollectionFieldValue(StringBuilder json, ICollectionField field, Object val) {
        Collection<?> coll = (Collection<?>) val;
        StringBuilder collJson = null;

        if (coll != null) {
            collJson = new StringBuilder();
            collJson.append('[');

            IDataObjectMeta itemObjectMeta = field.getItemObjectMeta();

            for (Object itemObj : coll) {
                String itemJson = serialize1(itemObjectMeta, itemObj);
                collJson.append(itemJson);
                collJson.append(',');
            }

            if (coll.size() > 0) {
                collJson.setCharAt(collJson.length() - 1, ']');
            } else {
                collJson.append(']');
            }
        }

        json.append(collJson);
        json.append(',');
    }

    private void addDataContractFlag(StringBuilder json, boolean flag) {
        json.append(resolveDataValue(flag ? 1 : 0));
        json.append(',');
    }

    private void addDataFieldValue(StringBuilder json, Object val) {
        json.append(resolveDataValue(val));
        json.append(',');
    }

    private void addObjectFieldValue(StringBuilder json, IDataObjectField field, Object val) {
        IDataObjectMeta objectMeta = field.getDataObjectMeta();
        String str = serialize1(objectMeta, val);
        json.append(str);
        json.append(',');
    }

    private void addStatusFieldValue(StringBuilder json, IDataContract dataContract) {
        StringBuilder statusFieldValue = new StringBuilder();

        statusFieldValue.append(dataContract.isReadOnly() ? 1 : 0);
        statusFieldValue.append(dataContract.getState().ordinal());
        statusFieldValue.append(dataContract.isFromDB() ? 1 : 0);
        statusFieldValue.append(resolveFormDBValue(dataContract));

        json.append(resolveDataValue(statusFieldValue.toString()));
        json.append(',');
    }

    private void addVersion(StringBuilder json) {
        json.append(resolveDataValue(VERSION));
        json.append(',');
    }

    private int calFieldSize(IDataObjectMeta objectMeta) {
        return objectMeta.getDataFields().size() + objectMeta.getDataObjectFields().size()
                + objectMeta.getCollectionFields().size() - objectMeta.getReferenceFields().size();
    }

    @SuppressWarnings("unchecked")
    private Object deserialize0(IDataObjectMeta objectMeta, List<Object> objectArray) {
        if (objectArray == null || objectArray.isEmpty()) {
            return null;
        }

        int index = 0;

        // check version

        String version = String.valueOf(objectArray.get(index++));
        if (!VERSION.equals(version)) {
            if (logger.isWarnEnabled()) {
                logger.warn("content version is not match!");
            }

            return null;
        }

        // check fieldSize
        int fieldSize = calFieldSize(objectMeta);

        // check datacontract flag
        boolean isDataContracrt = (int) objectArray.get(index++) == 1;
        String statusValue = null;

        // get statusValue
        if (isDataContracrt) {
            statusValue = (String) objectArray.get(index++);
            fieldSize = fieldSize + 3;
        } else {
            fieldSize = fieldSize + 2;
        }

        // get null field count
        int nullFieldCount = (int) objectArray.get(index++);
        NullFieldResolver nullFieldResolver = null;

        // get nullFieldValues
        if (nullFieldCount > 0) {
            List<Object> nullFieldValues = (List<Object>) objectArray.get(index++);
            nullFieldResolver = new NullFieldResolver(
                    ArrayUtils.toPrimitive(nullFieldValues.toArray(new Integer[nullFieldValues.size()])));
            fieldSize = fieldSize + 2 - nullFieldCount;
        } else {
            fieldSize = fieldSize + 1;
        }

        if (fieldSize != objectArray.size()) {
            if (logger.isWarnEnabled()) {
                logger.warn("content fields size is not match!");
            }

            return null;
        }

        Object dataObject = null;

        if (isDataContracrt) {
            try {
                DataContractGenerator.enable();
                dataObject = objectMeta.createDataObject();
            } finally {
                DataContractGenerator.disable();
            }
        } else {
            dataObject = objectMeta.createDataObject();
        }

        try {
            if (dataObject instanceof IInitialize) {
                ((IInitialize) dataObject).beginInit();
            }

            if (isDataContracrt) {
                setStatusFieldValue((IDataContract) dataObject, statusValue);
            }

            int fieldIndex = 0;

            for (IField field : objectMeta.getFields()) {
                // 引用对象忽略
                if (field instanceof IReferenceField) {
                    continue;
                }

                if (nullFieldResolver != null && nullFieldResolver.isNull(fieldIndex++)) {
                    continue;
                }

                if (field instanceof IDataField) {
                    Object val = objectArray.get(index++);
                    setDataFieldValue((IDataField) field, dataObject, val);
                } else if (field instanceof IDataObjectField) {
                    Object val = objectArray.get(index++);
                    setObjectFieldValue((IDataObjectField) field, dataObject, (List<Object>) val);
                } else if (field instanceof ICollectionField) {
                    Object val = objectArray.get(index++);
                    setCollectionFieldValue((ICollectionField) field, dataObject, (List<Object>) val);
                }

            }

            return dataObject;

        } finally {
            if (dataObject instanceof IInitialize) {
                ((IInitialize) dataObject).endInit();
            }
        }
    }

    private Map<?, ?> getFromDBMap(IDataContract dataContract) {
        Class<? extends IDataContract> cls = dataContract.getClass();
        Field fromDBMapField = ReflectionUtils.getDeclaredField(cls, "fromDBMap");
        Map<?, ?> fromDBMap = (Map<?, ?>) ReflectionUtils.getFieldValue(fromDBMapField, dataContract);
        return fromDBMap;
    }

    private String resolveDataValue(Object val) {
        if (val == null) {
            return null;
        }

        if (val instanceof String) {
            String strVal = (String) val;

            if (strVal.indexOf("\"") > -1) {
                try (SerializeWriter writer = new SerializeWriter()) {
                    writer.writeString(strVal);
                    return writer.toString();
                }
            } else {
                return String.join("", "\"", strVal, "\"");
            }
        }

        if (val instanceof Number) {
            return String.valueOf(val);
        }

        if (val instanceof Boolean) {
            return (boolean) val ? "true" : "false";
        }

        if (EnumUtils.isEnumObject(val)) {
            return String.join("", "\"", val.toString(), "\"");
        }

        if (val instanceof Date) {
            return String.valueOf(((Date) val).getTime());
        }

        if (val instanceof Boolean) {
            return (boolean) val ? "true" : "false";
        }

        if (val instanceof Collection<?>) {
            try (SerializeWriter writer = new SerializeWriter()) {
                writer.writeString(JSON.toJSONString(val));
                return writer.toString();
            }
        }

        return String.valueOf(val);
    }

    private String resolveFormDBValue(IDataContract dataContract) {
        Map<?, ?> fromDBMap = getFromDBMap(dataContract);
        if (fromDBMap == null || fromDBMap.isEmpty()) {
            return "";
        }

        StringBuffer fromDBStr = new StringBuffer();

        for (Entry<?, ?> entry : fromDBMap.entrySet()) {
            if (ConvertUtils.convert(entry.getValue(), Boolean.class)) {
                fromDBStr.append(entry.getKey());
                fromDBStr.append(',');
            }
        }

        int length = fromDBStr.length();
        if (length > 1) {
            fromDBStr.delete(length - 1, length);
        }

        return fromDBStr.toString();
    }

    private String serialize1(IDataObjectMeta objectMeta, Object dataObject) {
        if (dataObject == null) {
            return null;
        }

        StringBuilder json = new StringBuilder();
        json.append('[');

        // 头：序列化版本
        addVersion(json);

        if (dataObject instanceof IDataContract) {
            addDataContractFlag(json, true);
            addStatusFieldValue(json, (IDataContract) dataObject);
        } else {
            addDataContractFlag(json, false);
        }

        StringBuilder dataJson = new StringBuilder();

        // null字段标识器
        NullFieldResolver nullFieldResolver = new NullFieldResolver(this.calFieldSize(objectMeta));
        int fieldIndex = 0;

        for (IField field : objectMeta.getFields()) {
            // 引用对象忽略
            if (field instanceof IReferenceField) {
                continue;
            }

            Object val = field.getValue(dataObject);

            if (val == null) {
                // 为减少序列化内容，null不拼接到字符串里，而是记录这个字段的索引
                nullFieldResolver.markNull(fieldIndex);
            } else {
                if (field instanceof IDataField) {
                    addDataFieldValue(dataJson, val);
                } else if (field instanceof IDataObjectField) {
                    addObjectFieldValue(dataJson, (IDataObjectField) field, val);
                } else if (field instanceof ICollectionField) {
                    addCollectionFieldValue(dataJson, (ICollectionField) field, val);
                }
            }

            fieldIndex++;
        }

        int nullCount = nullFieldResolver.nullCount;
        json.append(nullCount);
        json.append(',');

        if (nullCount > 0) {
            json.append(nullFieldResolver.getValues());
            json.append(',');
        }

        json.append(dataJson.toString());

        int length = json.length();
        if (length > 1) {
            json.setCharAt(length - 1, ']');
        } else {
            json.append(']');
        }

        return json.toString();
    }

    private void setCollectionFieldValue(ICollectionField field, Object dataObject, List<Object> collArray) {
        if (collArray == null) {
            return;
        }

        IDataObjectMeta itemObjectMeta = field.getItemObjectMeta();

        @SuppressWarnings("unchecked")
        Collection<Object> list = (Collection<Object>) field.getValue(dataObject);

        for (Object item : collArray) {
            @SuppressWarnings("unchecked")
            List<Object> objectArray = (List<Object>) item;
            Object childObject = this.deserialize0(itemObjectMeta, objectArray);

            list.add(childObject);
        }
    }

    private void setDataFieldValue(IDataField field, Object dataObject, Object value) {
        if (value == null) {
            return;
        }

        Object resolveValue = ConvertUtils.convert(value, field.getFieldClass());
        field.setValue(dataObject, resolveValue);
    }

    private void setObjectFieldValue(IDataObjectField field, Object dataObject, List<Object> objectArray) {
        if (objectArray == null) {
            return;
        }

        IDataObjectMeta objectMeta = field.getDataObjectMeta();
        Object objValue = deserialize0(objectMeta, objectArray);
        field.setValue(dataObject, objValue);
    }

    private void setStatusFieldValue(IDataContract dataContract, String statusValue) {
        boolean readOnly = Integer.valueOf(statusValue.substring(0, 1)) == 1 ? true : false;
        dataContract.setReadOnly(readOnly);

        DataObjectState state = ConvertUtils.convert(Integer.valueOf(statusValue.substring(1, 2)),
                DataObjectState.class);
        dataContract.setState(state);

        boolean formDB = Integer.valueOf(statusValue.substring(2, 3)) == 1 ? true : false;
        dataContract.setFromDB(formDB);

        String fromDBTableString = statusValue.substring(3, statusValue.length());
        if (StringUtils.isEmpty(fromDBTableString)) {
            return;
        }

        String[] fromDBTables = fromDBTableString.split(",");
        if (fromDBTables.length > 0) {
            for (String subTable : fromDBTables) {
                dataContract.setFromDB(subTable, true);
            }
        }
    }

    /**
     * 记录每个字段是否是null.字段即其在元数据中的索引，比如有4个字段，第一个和第4个字段为null，
     * 则计数二进制为1001,即十进制的9，位运算的边界是int32，去掉符号位31位，因此，一个int最多可以表示31的字段的null标识
     *
     * @author hangwen
     */
    private static class NullFieldResolver {
        private static final int LIMIT = 31;

        // null标识值,每31个字段为一个值
        private int[] values;
        // 当前的值所在位置
        private int index = 0;
        // 当前位置对应的偏移量
        private int offset = LIMIT - 1;
        private int nullCount = 0;

        NullFieldResolver(int fieldCount) {
            int valueCount = PageUtils.getTotalPageCount(fieldCount, LIMIT);
            this.values = new int[valueCount];

            for (int i = 0; i < valueCount; i++) {
                this.values[i] = 0;
            }
        }

        NullFieldResolver(int[] values) {
            this.values = values;
        }

        String getValues() {
            int iMax = this.values.length - 1;

            StringBuilder b = new StringBuilder();
            b.append('[');
            for (int i = 0; ; i++) {
                b.append(this.values[i]);
                if (i == iMax)
                    return b.append(']').toString();
                b.append(',');
            }
        }

        boolean isNull(int fieldIndex) {
            if (fieldIndex > this.offset) {
                move();
            }

            int v = this.values[this.index];
            int flag = flag(fieldIndex);

            return (v & flag) == flag;
        }

        void markNull(int fieldIndex) {
            if (fieldIndex > this.offset) {
                move();
            }

            int v = this.values[this.index];
            int flag = flag(fieldIndex);

            this.values[this.index] = v | flag;

            this.nullCount++;
        }

        private int flag(int fieldIndex) {
            return 1 << (fieldIndex % LIMIT);
        }

        private void move() {
            this.index++;
            this.offset = (this.index + 1) * LIMIT - 1;
        }
    }

}
